﻿using System;
using System.Globalization;
using System.Windows.Data;

namespace Black.SurfaceToolkit.Converters
{
    /// <summary>
    /// Applique la transformation spécifiée.
    /// </summary>
    public abstract class FunctionMappingConverter : IValueConverter
    {
        #region Accesseurs

        /// <summary>
        /// Obtient ou définit l'abscisse du point 1.
        /// </summary>
        public double X1
        {
            get
            {
                return (this.m_dX1);
            }
            set
            {
                if (this.m_dX1 != value)
                {
                    this.m_dX1 = value;
                    this.OnControlPointsChanged();
                }
            }
        }

        /// <summary>
        /// Obtient ou définit l'abscisse du point 2.
        /// </summary>
        public double X2
        {
            get
            {
                return (this.m_dX2);
            }
            set
            {
                if (this.m_dX2 != value)
                {
                    this.m_dX2 = value;
                    this.OnControlPointsChanged();
                }
            }
        }

        /// <summary>
        /// Obtient ou définit le mode d'extrapolation du point 1.
        /// </summary>
        public ExtremityBehavior P1Behavior
        {
            get
            {
                return (this.m_eP1Behavior);
            }
            set
            {
                this.m_eP1Behavior = value;
            }
        }

        /// <summary>
        /// Obtient ou définit le mode d'extrapolation du point 2.
        /// </summary>
        public ExtremityBehavior P2Behavior
        {
            get
            {
                return (this.m_eP2Behavior);
            }
            set
            {
                this.m_eP2Behavior = value;
            }
        }

        /// <summary>
        /// Obtient ou définit une valeur indiquant s'il faut appliquer les comportements de fin tant que cela est possible ou une seule fois seulement.
        /// </summary>
        public bool EnsureBehaviors
        {
            get
            {
                return (this.m_bEnsureBehaviors);
            }
            set
            {
                this.m_bEnsureBehaviors = value;
            }
        }

        #endregion

        #region Variables d'instance

        private double m_dX1;
        private double m_dX2;

        private bool m_bEnsureBehaviors;
        private ExtremityBehavior m_eP1Behavior;
        private ExtremityBehavior m_eP2Behavior;

        #endregion
        
        #region Constructeurs

        /// <summary>
        /// Initalise une nouvelle instance de <see cref="T:Black.SurfaceToolkit.Converters.FunctionMappingConverter" />.
        /// </summary>
        public FunctionMappingConverter()
            : base()
        {
        }

        #endregion

        #region Fonctions protégées

        /// <summary>
        /// Appelée lorsqu'un des points de contrôle change.
        /// </summary>
        protected virtual void OnControlPointsChanged()
        {
        }

        #endregion

        #region Fonctions publiques

        /// <summary>
        /// Obtient l'image de l'abscisse spécifiée.
        /// </summary>
        /// <param name="p_oValue">Abscisse.</param>
        /// <param name="p_oTargetType">Type de destination.</param>
        /// <param name="p_oParameter">Paramètre de convertion.</param>
        /// <param name="p_oCulture">Culture de convertion.</param>
        /// <returns>Image de <paramref name="p_oValue" />.</returns>
        /// <exception cref="T:System.InvalidOperationException">La conversion n'est pas possible.</exception>
        public object Convert(object p_oValue, Type p_oTargetType, object p_oParameter, CultureInfo p_oCulture)
        {
            double p_dX = p_oValue is double ? (double)p_oValue : (double)System.Convert.ChangeType(p_oValue, typeof(double));

            double dXMin = this.m_dX1;
            double dXMax = this.m_dX2;

            ExtremityBehavior eMinBehavior = this.m_eP1Behavior;
            ExtremityBehavior eMaxBehavior = this.m_eP2Behavior;

            if (this.m_dX1 > this.m_dX2)
            {
                dXMin = this.m_dX2;
                dXMax = this.m_dX1;
                eMinBehavior = this.m_eP2Behavior;
                eMaxBehavior = this.m_eP1Behavior;
            }

            bool bChanged;

            do
            {
                bChanged = false;

                switch (eMinBehavior)
                {
                    case ExtremityBehavior.Repeat:
                        if (dXMax != dXMin && p_dX < dXMin)
                        {
                            double dDelta = dXMax - dXMin;
                            double dOffset = dXMax - p_dX;
                            double dCountStep = (int)(dOffset / dDelta);

                            if (dCountStep != 0)
                            {
                                p_dX = dXMax - (dOffset - dCountStep * dDelta);
                                bChanged = true;
                            }
                        }
                        break;

                    case ExtremityBehavior.Period:
                        if (dXMax != dXMin && p_dX < dXMin)
                        {
                            double dDelta = dXMax - dXMin;
                            double dOffset = dXMax - p_dX;
                            int iCountStep = (int)(dOffset / dDelta);

                            if (iCountStep != 0)
                            {
                                iCountStep -= iCountStep % 2;
                                p_dX = dXMax - (dOffset - iCountStep * dDelta);

                                if (p_dX < dXMin)
                                {
                                    p_dX = dXMin + (dXMin - p_dX);
                                    bChanged = true;
                                }
                            }
                        }
                        break;

                    case ExtremityBehavior.Hold:
                        if (p_dX < dXMin)
                        {
                            p_dX = dXMin;
                            bChanged = true;
                        }
                        break;

                    case ExtremityBehavior.Invert:
                        if (p_dX < dXMin)
                        {
                            p_dX = dXMin + (dXMin - p_dX);
                            bChanged = true;
                        }
                        break;
                }

                // Un seule changement dans se cas

                if (bChanged && !this.m_bEnsureBehaviors)
                    break;

                switch (eMaxBehavior)
                {
                    case ExtremityBehavior.Repeat:
                        if (dXMax != dXMin && p_dX > dXMax)
                        {
                            double dDelta = dXMax - dXMin;
                            double dOffset = p_dX - dXMin;
                            int iCountStep = (int)(dOffset / dDelta);

                            if (iCountStep != 0)
                            {
                                p_dX = dOffset - iCountStep * dDelta + dXMin;
                                bChanged = true;
                            }
                        }
                        break;

                    case ExtremityBehavior.Period:
                        if (dXMax != dXMin && p_dX > dXMax)
                        {
                            double dDelta = dXMax - dXMin;
                            double dOffset = p_dX - dXMin;
                            int iCountStep = (int)(dOffset / dDelta);

                            if (iCountStep != 0)
                            {
                                iCountStep -= iCountStep % 2;
                                p_dX = dOffset - iCountStep * dDelta + dXMin;
                                bChanged = true;

                                if (p_dX > dXMax)
                                {
                                    p_dX = dXMax + (dXMax - p_dX);
                                    bChanged = true;
                                }
                            }
                        }
                        break;

                    case ExtremityBehavior.Hold:
                        if (p_dX > dXMax)
                        {
                            p_dX = dXMax;
                            bChanged = true;
                        }
                        break;

                    case ExtremityBehavior.Invert:
                        if (p_dX > dXMax)
                        {
                            p_dX = dXMax + (dXMax - p_dX);
                            bChanged = true;
                        }
                        break;
                }
            } while (bChanged && this.m_bEnsureBehaviors);

            // Détermination du résultat

            double dResult = this.GetImage(p_dX);

            if (p_oTargetType == typeof(Double))
                return (dResult);

            return (System.Convert.ChangeType(dResult, p_oTargetType));
        }

        /// <summary>
        /// Obtient l'abscisse de l'image spécifiée. Ne prend pas en compte les modes d'extrapolation.
        /// </summary>
        /// <param name="p_oValue">Image.</param>
        /// <param name="p_oTargetType">Types de destination.</param>
        /// <param name="p_oParameter">Paramètre de convertion.</param>
        /// <param name="p_oCulture">Culture de convertio.</param>
        /// <returns>Abscisse de <paramref name="p_oValue" />.</returns>
        /// <exception cref="T:System.InvalidOperationException">La conversion n'est pas possible.</exception>
        public object ConvertBack(object p_oValue, Type p_oTargetType, object p_oParameter, CultureInfo p_oCulture)
        {
            double dY = p_oValue is double ? (double)p_oValue : (double)System.Convert.ChangeType(p_oValue, typeof(double));
            double dResult = this.GetAbscissa(dY);

            if (p_oTargetType == typeof(Double))
                return (dResult);

            return (System.Convert.ChangeType(dResult, p_oTargetType));
        }

        /// <summary>
        /// Obtient l'abscisse de l'image spécifiée.
        /// </summary>
        /// <param name="p_dY">Image du point.</param>
        /// <returns>Abscisse de <paramref name="p_dY" />.</returns>
        /// <exception cref="T:System.InvalidOperationException">L'obtention de l'abscisse n'est pas possible.</exception>
        public abstract double GetAbscissa(double p_dY);

        /// <summary>
        /// Obtient l'image de l'abscisse spécifiée.
        /// </summary>
        /// <param name="p_dX">Abscisse du point.</param>
        /// <returns>Image de <paramref name="p_dX" />.</returns>
        /// <exception cref="T:System.InvalidOperationException">L'obtention de l'image n'est pas possible.</exception>
        public abstract double GetImage(double p_dX);

        #endregion
    }
}
